#include <QDataStream>
#include <QtEndian>
#include <cstring>

#include <iostream>

#include "client_connection.h"
#include "client_print.h"

const quint32 CClientConnection::sMaxNetStringLen = 1024;

CClientConnection::CClientConnection(QString& pHost, quint16& pPort) :
	mHost(pHost),
	mPort(pPort),
	mProtocolState(PROTOCOL_LENGTH),
	mClientState(CLIENT_DISCONNECTED),
	mNumBytesToRead(4),
	mTcpSocket(),
	mSync(QMutex::Recursive),
	mCommandSendSync(QMutex::Recursive)
{
	mTcpSocket.connectToHost(pHost, pPort);
	connect(&mTcpSocket, SIGNAL(connected()),
			this, SLOT(connected()));
	connect(&mTcpSocket, SIGNAL(disconnected()),
			this, SLOT(disconnected()));
	connect(&mTcpSocket, SIGNAL(readyRead()),
			this, SLOT(readyRead()));
	connect(&mTcpSocket, SIGNAL(error(QAbstractSocket::SocketError)),
			this, SLOT(error(QAbstractSocket::SocketError)));
}

CClientConnection::~CClientConnection()
{
	if (mTcpSocket.isOpen())
	{
		mTcpSocket.close();
	}
	disconnect(&mTcpSocket, SIGNAL(connected()),
			this, SLOT(connected()));
	disconnect(&mTcpSocket, SIGNAL(disconnected()),
			this, SLOT(disconnected()));
	disconnect(&mTcpSocket, SIGNAL(readyRead()),
			this, SLOT(readyRead()));
	disconnect(&mTcpSocket, SIGNAL(error(QAbstractSocket::SocketError)),
			this, SLOT(error(QAbstractSocket::SocketError)));
}

bool CClientConnection::requestStart()
{
	quint8 lStartCmd(COMMAND_START);
	if (sendCommand(&lStartCmd, 1) < 1)
	{
		return false;
	}

	mClientState = CLIENT_STARTED;
	return true;
}

bool CClientConnection::requestConfig()
{
	if (mClientState == CLIENT_CONNECTED)
	{
		quint8 lRequestConfigCmd(COMMAND_TRANSMIT_CONFIG);
		if (sendCommand(&lRequestConfigCmd, 1) < 1)
		{
			return false;
		}
		return true;
	}

	return false;
}

bool CClientConnection::sendDefenseValue(quint32 pValue)
{
	if (mClientState != CLIENT_DISCONNECTED)
	{
		quint8 lNoiseRecvAmpCmd[5] = { COMMAND_NOISE_RECV_AMP, 0, 0, 0, 0 };
		qToBigEndian(pValue, (lNoiseRecvAmpCmd + 1));
		if (sendCommand(lNoiseRecvAmpCmd, 5) < 5)
		{
			return false;
		}
		return true;
	}

	return false;
}

bool CClientConnection::sendConfigSection(const QString& pSection)
{
	if (mClientState != CLIENT_CONNECTED)
	{
		return false;
	}

	quint8 lData[sMaxNetStringLen];
	quint32 lDataLen(0);

	lData[0] = COMMAND_RECEIVE_CONFIG_SECTION;
	strcpy(reinterpret_cast<char*>(lData + 1), pSection.toStdString().c_str());
	/* DataLength = 1 (cmd) + strlen + 1 (0-byte) */
	lDataLen = 1 + pSection.length() + 1;

	if (sendCommand(lData, lDataLen) < lDataLen)
	{
		return false;
	}

	return true;
}

bool CClientConnection::sendConfigKey(const QString& pSection, const QString& pKey, const QString& pValue)
{
	if (mClientState != CLIENT_CONNECTED)
	{
		return false;
	}

	quint8 lData[sMaxNetStringLen];
	quint32 lDataLen(0);
	
	/* DataLength = 1 (cmd) + strlen(pSection) + 1 (":") + strlen(pKey) + 1 ("=") + strlen(pValue) */
	lData[0] = COMMAND_RECEIVE_CONFIG_KEY;
	lDataLen = 1;

	strcpy(reinterpret_cast<char*>(lData + 1), pSection.toStdString().c_str());
	lDataLen += pSection.length();
	*(lData + lDataLen) = ':';
	lDataLen += 1;
	strcpy(reinterpret_cast<char*>(lData + lDataLen), pKey.toStdString().c_str());
	lDataLen += pKey.length();
	*(lData + lDataLen) = '=';
	lDataLen += 1;
	strcpy(reinterpret_cast<char*>(lData + lDataLen), pValue.toStdString().c_str());
	lDataLen += pValue.length();
	*(lData + lDataLen) = '\0';
	lDataLen += 1; /* 0-byte */

	if (sendCommand(lData, lDataLen) < lDataLen)
	{
		return false;
	}

	return true;
}

bool CClientConnection::sendConfigStart()
{
	if (mClientState == CLIENT_CONNECTED)
	{
		quint8 lSendConfigStartCmd(COMMAND_RECEIVE_CONFIG_START);
		if (sendCommand(&lSendConfigStartCmd, 1) < 1)
		{
			return false;
		}
		return true;
	}

	return false; 
}

bool CClientConnection::sendConfigEnd()
{
	if (mClientState == CLIENT_CONNECTED)
	{
		quint8 lSendConfigEndCmd(COMMAND_RECEIVE_CONFIG_END);
		if (sendCommand(&lSendConfigEndCmd, 1) < 1)
		{
			return false;
		}
		return true;
	}
	return false; 
}

void CClientConnection::connected()
{
	mClientState = CLIENT_CONNECTED;
	emit clientConnected(mHost, mPort);
}

void CClientConnection::disconnected()
{
	mClientState = CLIENT_DISCONNECTED;
	emit clientDisconnected();
}

void CClientConnection::error(QAbstractSocket::SocketError pSocketError)
{
	CClientPrint::print("QTcpSocket error");
	emit connectionError(QString::number(pSocketError, 10));
	/* FIXME handle state */
}

void CClientConnection::protocolReadDataSend(const void* pData, int pLength)
{
	if (!pData || pLength <= 0) {
		return;
	}
	quint8* lData(new quint8 [pLength + 1]);
	lData[0] = COMMAND_PROTOCOL_READ_DATA;
	for (int i = 0; i < pLength; i++) {
		lData[i + 1] = static_cast<const quint8*>(pData)[i];
	}
	sendCommand(lData, pLength + 1);
	delete [] lData;
}

void CClientConnection::readyRead()
{
	QDataStream lIn(&mTcpSocket);
	lIn.setVersion(QDataStream::Qt_4_6);
	
	mSync.lock();
	if (mTcpSocket.bytesAvailable() <= 0) {
		mSync.unlock();
		return;
	}

	do  
	{
		if(mProtocolState == PROTOCOL_LENGTH &&
				mTcpSocket.bytesAvailable() >= mNumBytesToRead)
		{
			if (mNumBytesToRead != 4) {
				CClientPrint::print("Length error");
			}
			//if (mTcpSocket.read(reinterpret_cast<char*>(&mNumBytesToRead), 4) != 4) {
			if (lIn.readRawData(reinterpret_cast<char*>(&mNumBytesToRead), 4) != 4) {
				CClientPrint::print("Couldn't read 4 bytes");
			}
			mNumBytesToRead = qFromBigEndian(mNumBytesToRead);
			//lIn >> mNumBytesToRead; // this could be use instead of readRawData and qFromBigEndian
			mProtocolState = PROTOCOL_PAYLOAD;
		}   
		if(mProtocolState == PROTOCOL_PAYLOAD &&
				mTcpSocket.bytesAvailable() >= mNumBytesToRead)
		{
			char* lPayloadBuffer = new char[mNumBytesToRead + 1];
			//if (mTcpSocket.read(lPayloadBuffer, mNumBytesToRead) != static_cast<qint32>(mNumBytesToRead)) {
			if (lIn.readRawData(lPayloadBuffer, mNumBytesToRead) != static_cast<qint32>(mNumBytesToRead)) {
				CClientPrint::print("Could not read " + QString::number(mNumBytesToRead) + " bytes");
			}
			//printData(lOffset, reinterpret_cast<quint8*>(lPayloadBuffer), mNumBytesToRead);
			lPayloadBuffer[mNumBytesToRead] = '\0';
			mProtocolState = PROTOCOL_LENGTH;
			uint lPayloadLength(mNumBytesToRead); // length without '\0' !
			mNumBytesToRead = 4;

			if (!receivedCommand(lPayloadBuffer, lPayloadLength)) {
				CClientPrint::print("Received close connection command.");

				delete [] lPayloadBuffer;
				mSync.unlock();
				return;
			}

			delete [] lPayloadBuffer;
		}   
	}   
	while(mTcpSocket.bytesAvailable() >= mNumBytesToRead);
	mSync.unlock();
}

bool CClientConnection::receivedCommand(char* pData, unsigned int pLen)
{
	QString lCmdData("");
	QString lSection("");
	QString lKey("");
	QString lValue("");

	switch(pData[0])
	{
		case COMMAND_CLOSE_CONNECTION:
			emit closeReceived();
			mTcpSocket.close();
			return false;
		case COMMAND_START:
			break;
		case COMMAND_TRANSMIT_CONFIG:
			lCmdData.append(reinterpret_cast<const char*>(pData + 1));
			lSection = lCmdData.left(lCmdData.indexOf(":"));
			lKey = lCmdData.mid(lCmdData.indexOf(":") + 1, lCmdData.indexOf("=") - lCmdData.indexOf(":") - 1);
			lValue = lCmdData.mid(lCmdData.indexOf("=") + 1);
			emit configReceived(lSection, lKey, lValue);
			break;
		case COMMAND_TRANSMIT_INFO:
			lCmdData.append(reinterpret_cast<const char*>(pData + 2));
			emit infoReceived(static_cast<ELogLevel>(pData[1]), lCmdData);
			break;
		case COMMAND_EXTRACT_BUFFER_INFO:
		{
			quint32 lUsed(qFromBigEndian(*reinterpret_cast<quint32*>(pData + 1)));
			quint32 lSize(qFromBigEndian(*reinterpret_cast<quint32*>(pData + 5)));
			emit extractBufferInfoReceived(lUsed, lSize);
		}
			break;
		case COMMAND_FRAME_SEND_LENGTH:
			emit frameSendLengthReceived(qFromBigEndian(*reinterpret_cast<quint32*>(pData + 1)));
			break;
		case COMMAND_FRAME_RECV_LENGTH:
			emit frameRecvLengthReceived(
					qFromBigEndian(*reinterpret_cast<quint32*>(pData + 1)),
					pData[5] ? true : false);
			break;
		case COMMAND_PACKET_SEND_LENGTH:
			emit packetSendLengthReceived(qFromBigEndian(*reinterpret_cast<quint32*>(pData + 1)));
			break;
		case COMMAND_PACKET_RECV_LENGTH:
			emit packetRecvLengthReceived(
					qFromBigEndian(*reinterpret_cast<quint32*>(pData + 1)),
					pData[5] ? true : false);
			break;
		case COMMAND_STREAM_SEND_INFO:
		{
			quint32 lLength(qFromBigEndian(*reinterpret_cast<quint32*>(pData + 3)));
			emit streamSendInfoReceived(pData[1], pData[2], lLength,
					pData[7] ? true : false);
		}
			break;
		case COMMAND_STREAM_SEND_DATA:
			emit streamSendDataReceived(&pData[1], pLen - 1);
			break;
		case COMMAND_STREAM_RECV_INFO:
		{
			quint32 lLength(qFromBigEndian(*reinterpret_cast<quint32*>(pData + 3)));
			emit streamRecvInfoReceived(pData[1], pData[2], lLength,
					pData[7] ? true : false);
		}
			break;
		case COMMAND_STREAM_RECV_DATA:
			emit streamRecvDataReceived(&pData[1], pLen - 1);
			break;
		case COMMAND_PROTOCOL_WRITE_DATA:
			emit protocolWriteDataReceived(&pData[1], pLen - 1);
			break;
		case COMMAND_PROTOCOL_OPEN:
			break;
		case COMMAND_PROTOCOL_CLOSE:
			break;
		case COMMAND_PROTOCOL_READ_FEEDBACK:
		{
			quint32 lRead(qFromBigEndian(*reinterpret_cast<quint32*>(pData + 1)));
			quint32 lSize(qFromBigEndian(*reinterpret_cast<quint32*>(pData + 5)));
			emit protocolReadFeedbackReceived(lRead, lSize);
		}
			break;
		case COMMAND_AMP_EMBED:
			emit embedAmpReceived(static_cast<quint8>(pData[1]));
			break;
		case COMMAND_AMP_EXTRACT:
			emit extractAmpReceived(static_cast<quint8>(pData[1]));
			break;

		default:
			CClientPrint::print("Received unknown command " +
				QString::number(pData[0]));
			break;
	}

	return true;
}

qint64 CClientConnection::sendCommand(quint8* pData, quint32 pLen)
{
	qint64 lReturn(0);

	if (mClientState != CLIENT_DISCONNECTED)
	{
		QByteArray lBlock;
		QDataStream lOut(&lBlock, QIODevice::WriteOnly);
		lOut.setVersion(QDataStream::Qt_4_6);
		lOut << pLen;
		for(quint32 lCnt = 0; lCnt < pLen; lCnt++)
		{
			lOut << pData[lCnt];
		}

		mCommandSendSync.lock();
		//mSync.lock(); // should not be necessary
		// --> read and write at same time is possible
		lReturn = mTcpSocket.write(lBlock);
		//mSync.unlock(); // should not be necessary
		mCommandSendSync.unlock();
	}
	else
	{
		lReturn = -1; /* error */
	}

	return lReturn;
}

void CClientConnection::printData(unsigned int pOffset, quint8* pData,
		unsigned int pLen)
{
	quint32 lOffset(pOffset);
	lOffset = qToBigEndian(lOffset);
	CClientPrint::print("offset: " +
			toHex(reinterpret_cast<unsigned char*>(&lOffset)[0]) +
			toHex(reinterpret_cast<unsigned char*>(&lOffset)[1]) +
			toHex(reinterpret_cast<unsigned char*>(&lOffset)[2]) +
			toHex(reinterpret_cast<unsigned char*>(&lOffset)[3]));
	if (!pLen) {
		return;
	}
	for (unsigned int row = 0; row <= ((pLen - 1) / 10); row++) {
		unsigned int lMax = (row + 1) * 10;
		if (lMax > pLen) {
			lMax = pLen;
		}
		QString lValues;
		for (unsigned int i = row * 10; i < lMax; i++) {
			lValues += toHex(pData[i]);
		}
		CClientPrint::print(lValues);
	}
}

QString CClientConnection::toHex(quint8 pValue)
{
	if (pValue < 0x10) {
		return " 0" + QString::number(pValue, 16);
	}

	return " " + QString::number(pValue, 16);
}
